
#import "EventsGeoRSSImporter.h"
#import "NBJEvent.h"
#import <libxml/tree.h>

// Function prototypes for SAX callbacks. This sample implements a minimal subset of SAX callbacks.
// Depending on your application's needs, you might want to implement more callbacks.
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes);
static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI);
static void charactersFoundSAX(void *context, const xmlChar *characters, int length);
static void errorEncounteredSAX(void *context, const char *errorMessage, ...);

// Forward reference. The structure is defined in full at the end of the file.
static xmlSAXHandler simpleSAXHandlerStruct;

// Class extension for private properties and methods.
@interface EventsGeoRSSImporter ()

@property BOOL storingCharacters;
@property (nonatomic, retain) NSMutableData *characterBuffer;
@property BOOL done;
@property BOOL parsingAnEvent;
@property NSUInteger countForCurrentBatch;
@property (nonatomic, retain) NBJEvent *currentEvent;
@property (nonatomic, retain) NSURLConnection *rssConnection;

// The autorelease pool property is assign because autorelease pools cannot be retained.
@property (nonatomic, assign) NSAutoreleasePool *importPool;

- (void) forwardError:(NSError *)error;

@end

static double lookuptime = 0;

@implementation EventsGeoRSSImporter

@synthesize delegate, persistentStoreCoordinator;
@synthesize rssConnection, done, parsingAnEvent, storingCharacters, currentEvent, countForCurrentBatch, characterBuffer, importPool;

- (void) dealloc 
{
	[nbjEpochTime release];
    [characterBuffer release];
    [currentEvent release];
    [rssConnection release];
    [persistentStoreCoordinator release];
    [insertionContext release];
    [eventEntityDescription release];
    [super dealloc];
}

- (void) main 
{
    self.importPool = [[NSAutoreleasePool alloc] init];
    if (delegate && [delegate respondsToSelector:@selector(importerDidSave:)]) {
        [[NSNotificationCenter defaultCenter] addObserver:delegate selector:@selector(importerDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
    }
	
	NSManagedObjectContext *moc = self.insertionContext;
	
	// Delete all existing objects
	NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
	[fetchRequest setEntity:[NSEntityDescription entityForName:@"Event" inManagedObjectContext:moc]];
	NSArray *events = [moc executeFetchRequest:fetchRequest error:NULL];
	for(NBJEvent *event in events) {
		[moc deleteObject:event];
	}
	[fetchRequest release];
	
    done = NO;
    self.characterBuffer = [NSMutableData data];

    // create the connection with the request and start loading the data
	NSURL *feedURL = [[NSURL alloc] initWithString:@"http://maps.google.com/maps/ms?ie=UTF8&msa=0&output=georss&msid=102125296625596679496.0004563675b2aac240bd5"];
	NSURLRequest *theRequest = [[NSURLRequest alloc] initWithURL:feedURL];
    rssConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
	[theRequest release];
	[feedURL release];
	
    // This creates a context for "push" parsing in which chunks of data that are not "well balanced" can be passed
    // to the context for streaming parsing. The handler structure defined above will be used for all the parsing. 
    // The second argument, self, will be passed as user data to each of the SAX handlers. The last three arguments
    // are left blank to avoid creating a tree in memory.
    context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, self, NULL, 0, NULL);
    if (rssConnection != nil) {
        do {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        } while (!done);
    }
	
    // Display the total time spent finding a specific object for a relationship
    NSLog(@"lookup time %f", lookuptime);
	
    // Release resources used only in this thread.
    xmlFreeParserCtxt(context);
    self.characterBuffer = nil;
    self.rssConnection = nil;
    self.currentEvent = nil;
	
    NSError *saveError = nil;
	if(![moc save:&saveError]) {
		[self forwardError:saveError];
	} else {
		if (delegate && [delegate respondsToSelector:@selector(importerDidSave:)]) {
			[[NSNotificationCenter defaultCenter] removeObserver:delegate name:NSManagedObjectContextDidSaveNotification object:moc];
		}
		
		if (self.delegate != nil && [self.delegate respondsToSelector:@selector(importerDidFinishParsingData:)]) {
			[self.delegate importerDidFinishParsingData:self];
		}
	}
	
    [importPool release];
    self.importPool = nil;
}

- (NSDate *) nbjEpochTime
{
	if(nbjEpochTime == nil) {
		// NBJ Epoch time is +237340800 seconds from OS X Epoch time
		nbjEpochTime = [[NSDate alloc] initWithTimeIntervalSinceReferenceDate:237340800];
	}
	
	return nbjEpochTime;
}

- (NSManagedObjectContext *) insertionContext 
{
    if (insertionContext == nil) {
        insertionContext = [[NSManagedObjectContext alloc] init];
        [insertionContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
    }
    return insertionContext;
}

- (void) forwardError:(NSError *)error 
{
    if (self.delegate != nil && [self.delegate respondsToSelector:@selector(importer:didFailWithError:)]) {
        [self.delegate importer:self didFailWithError:error];
    }
}

- (NSEntityDescription *) eventEntityDescription 
{
    if (eventEntityDescription == nil) {
        eventEntityDescription = [[NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.insertionContext] retain];
    }
    return eventEntityDescription;
}

- (NBJEvent *) currentEvent 
{
    if (currentEvent == nil) {
        currentEvent = [[NBJEvent alloc] initWithEntity:self.eventEntityDescription insertIntoManagedObjectContext:self.insertionContext];
    }
    return currentEvent;
}

#pragma mark NSURLConnection Delegate methods

// Forward errors to the delegate.
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{
    [self performSelectorOnMainThread:@selector(forwardError:) withObject:error waitUntilDone:NO];
    // Set the condition which ends the run loop.
    done = YES;
}

// Called when a chunk of data has been downloaded.
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
{
    // Process the downloaded chunk of data.
    xmlParseChunk(context, (const char *)[data bytes], [data length], 0);
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection 
{
    // Signal the context that parsing is complete by passing "1" as the last parameter.
    xmlParseChunk(context, NULL, 0, 1);
    context = NULL;
    // Set the condition which ends the run loop.
    done = YES; 
}

#pragma mark Parsing support methods

static const NSUInteger kImportBatchSize = 20;

- (void) finishedCurrentEvent 
{
    parsingAnEvent = NO;
    self.currentEvent = nil;
    countForCurrentBatch++;
	
    // Periodically purge the autorelease pool and save the context. The frequency of this action may need to be tuned according to the 
    // size of the objects being parsed. The goal is to keep the autorelease pool from growing too large, but 
    // taking this action too frequently would be wasteful and reduce performance.
    if (countForCurrentBatch == kImportBatchSize) {
        [importPool release];
        self.importPool = [[NSAutoreleasePool alloc] init];

		
        NSError *error = nil;
		if(![insertionContext save:&error]) {
			done = YES;
			
			NSLog(@"Failed to save to data store: %@", [error localizedDescription]);
			NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
			if(detailedErrors != nil && [detailedErrors count] > 0) {
				for(NSError* detailedError in detailedErrors) {
					NSLog(@"  DetailedError: %@", [detailedError userInfo]);
				}
			} else {
				NSLog(@"  %@", [error userInfo]);
			}
		}
		
        countForCurrentBatch = 0;
    }
}

/*
 Character data is appended to a buffer until the current element ends.
 */
- (void) appendCharacters:(const char *)charactersFound length:(NSInteger)length 
{
    [characterBuffer appendBytes:charactersFound length:length];
}

- (NSString *) currentString 
{
    // Create a string with the character data using UTF-8 encoding. UTF-8 is the default XML data encoding.
    NSString *currentString = [[[NSString alloc] initWithData:characterBuffer encoding:NSUTF8StringEncoding] autorelease];
    [characterBuffer setLength:0];
    return currentString;
}

@end

#pragma mark SAX Parsing Callbacks

// The following constants are the XML element names and their string lengths for parsing comparison.
// The lengths include the null terminator, to ensure exact matches.
static const char *kName_Item = "item";
static const NSUInteger kLength_Item = 5;
static const char *kName_Guid = "guid";
static const NSUInteger kLength_Guid = 5;
static const char *kName_Title = "title";
static const NSUInteger kLength_Title = 6;
static const char *kName_Address = "description";
static const NSUInteger kLength_Address = 12;
static const char *kName_Geo = "georss";
static const NSUInteger kLength_Geo = 7;
static const char *kName_GeoPoint = "point";
static const NSUInteger kLength_GeoPoint = 6;

// Called when parser encounters the beginning of a node, we can determine if we care about it and have the parser gather data.
static void startElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, 
                            int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) {
    EventsGeoRSSImporter *importer = (EventsGeoRSSImporter *)parsingContext;
    if (prefix == NULL && !strncmp((const char *)localname, kName_Item, kLength_Item)) {
        importer.parsingAnEvent = YES;
    } else if (importer.parsingAnEvent && 
			   ( (prefix == NULL && 
				  (!strncmp((const char *)localname, kName_Guid, kLength_Guid)) || 
				  (!strncmp((const char *)localname, kName_Title, kLength_Title)) ||
				  (!strncmp((const char *)localname, kName_Address, kLength_Address)) ||
				  (!strncmp((const char *)localname, kName_GeoPoint, kLength_GeoPoint)) ))) {
        importer.storingCharacters = YES;
    }
}

// Called when parser reaches the end of a node. This is where we gather all the data to fill our business objects
static void endElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) {    
    EventsGeoRSSImporter *importer = (EventsGeoRSSImporter *) parsingContext;
    if (importer.parsingAnEvent == NO) return;
	
	NBJEvent *tempEvent = importer.currentEvent;
	NSDate *nbjEpochTime = importer.nbjEpochTime;
	
	if (prefix == NULL) {
		if (!strncmp((const char *)localname, kName_Item, kLength_Item)) {
			[importer finishedCurrentEvent];
		} else if (!strncmp((const char *)localname, kName_Guid, kLength_Guid)) {
			tempEvent.guid = importer.currentString;
		} else if (!strncmp((const char *)localname, kName_Title, kLength_Title)) {
			// Split string in two tokens (name , bar number)
			NSArray *tokens = [importer.currentString arrayOfCaptureComponentsMatchedByRegex:@"^(.+)\\(Bar n°(\\d+).*\\)$"];
			if([tokens count] == 1 && [[tokens objectAtIndex:0] count] == 3) {
				NSArray *innerTokens = [tokens objectAtIndex:0];
				tempEvent.name = [[innerTokens objectAtIndex:1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
				NSInteger barNumber = [[innerTokens objectAtIndex:2] integerValue];
				tempEvent.eventId = [NSNumber numberWithInteger:barNumber];
				
				// Bar 1 == 2008-07-10, Bar 63 == 2009-09-17
				NSDate *eventDate = [[NSDate alloc] initWithTimeInterval:(3600 * 24 * 7 * (barNumber - 1)) sinceDate:nbjEpochTime];
				tempEvent.date = eventDate;
				[eventDate release];
			} else {
				tempEvent.name = @"";
				tempEvent.eventId = [NSNumber numberWithInteger:0];
			}
		} else if (!strncmp((const char *)localname, kName_Address, kLength_Address)) {
			// Clean up address by stripping HTML tags, trailing spaces and line feeds
			NSMutableString *address = [importer.currentString mutableCopy];
			[address replaceOccurrencesOfRegex:@"<(.|\\n)*?>" withString:@" "];
			[address replaceOccurrencesOfRegex:@"[\r\n\t]" withString:@" "];
			tempEvent.address = address;
			[address release];
		}
	} else if(!strncmp((const char *)prefix, kName_Geo, kLength_Geo)) {
		if (!strncmp((const char *)localname, kName_GeoPoint, kLength_GeoPoint)) {
			// Clean up <georss:point> by removing trailing space / line feeds and splitting in two tokens
			NSString *geopoint = [importer.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
			NSArray *tokens = [geopoint componentsSeparatedByString:@" "];
			if([tokens count] == 2) {
				tempEvent.latitude = [NSDecimalNumber decimalNumberWithString:[tokens objectAtIndex:0]];
				tempEvent.longitude = [NSDecimalNumber decimalNumberWithString:[tokens objectAtIndex:1]];
			}
		}
	}

	importer.storingCharacters = NO;
}

static void charactersFoundSAX(void *parsingContext, const xmlChar *characterArray, int numberOfCharacters) 
{
    EventsGeoRSSImporter *importer = (EventsGeoRSSImporter *)parsingContext;
    // A state variable, "storingCharacters", is set when nodes of interest begin and end. 
    // This determines whether character data is handled or ignored. 
    if (importer.storingCharacters == NO) return;
    [importer appendCharacters:(const char *)characterArray length:numberOfCharacters];
}

static void errorEncounteredSAX(void *parsingContext, const char *errorMessage, ...) 
{
    // Handle errors as appropriate for your application.
    NSCAssert(NO, @"Unhandled error encountered during SAX parse.");
}

// The handler struct has positions for a large number of callback functions. If NULL is supplied at a given position,
// that callback functionality won't be used. Refer to libxml documentation at http://www.xmlsoft.org for more information
// about the SAX callbacks.
static xmlSAXHandler simpleSAXHandlerStruct = {
	NULL,                       /* internalSubset */
	NULL,                       /* isStandalone   */
	NULL,                       /* hasInternalSubset */
	NULL,                       /* hasExternalSubset */
	NULL,                       /* resolveEntity */
	NULL,                       /* getEntity */
	NULL,                       /* entityDecl */
	NULL,                       /* notationDecl */
	NULL,                       /* attributeDecl */
	NULL,                       /* elementDecl */
	NULL,                       /* unparsedEntityDecl */
	NULL,                       /* setDocumentLocator */
	NULL,                       /* startDocument */
	NULL,                       /* endDocument */
	NULL,                       /* startElement*/
	NULL,                       /* endElement */
	NULL,                       /* reference */
	charactersFoundSAX,         /* characters */
	NULL,                       /* ignorableWhitespace */
	NULL,                       /* processingInstruction */
	NULL,                       /* comment */
	NULL,                       /* warning */
	errorEncounteredSAX,        /* error */
	NULL,                       /* fatalError //: unused error() get all the errors */
	NULL,                       /* getParameterEntity */
	NULL,                       /* cdataBlock */
	NULL,                       /* externalSubset */
	XML_SAX2_MAGIC,             //
	NULL,
	startElementSAX,            /* startElementNs */
	endElementSAX,              /* endElementNs */
	NULL,                       /* serror */
};
